﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Pavilion
{
    public enum RunningStatus { Running, Stopped, Paused }
    public class ContainerNodeMonitor
    {
        public event Action<ContainerNodeMonitor, NodeBase> BubbleUpNodeLoaded, BubbleUpNodeUnloaded, BubbleUpNodeExecuted;

        public RunningStatus RunningStatus { get; private set; }
        public NodeScript ParentNodeScript { get; private set; }
        public NodeBase CurrentNode { get; private set; }
        public NodeBase PendingNode { get; private set; }
        public NodeBase RootCurrentNode { get { return (CurrentNode is ContainerNode) ? CurrentContainerNodeMonitor.RootCurrentNode : CurrentNode; } }
        public bool IsExecuting { get { return GetIsExecuting(); } }
        public ContainerNodeMonitor CurrentContainerNodeMonitor { get { return containerNodeMonitorDictionary[(ContainerNode)CurrentNode]; } }

        private Dictionary<ContainerNode, ContainerNodeMonitor> containerNodeMonitorDictionary;
        private ContainerNode containerNode;
        private GameItem gameItem;
        private List<LinkMonitor> monitoredLinks;
        private Logger tracker;
        private bool isExecuting, isUpdating;

        public ContainerNodeMonitor(NodeScript parentNodeScript, ContainerNode containerNode, GameItem gameItem, Logger tracker)
        {
            RunningStatus = RunningStatus.Stopped;
            this.ParentNodeScript = parentNodeScript;
            this.containerNode = containerNode;
            this.tracker = tracker;
            tracker.IsEnabled = true;

            if (!string.IsNullOrEmpty(containerNode.GameItemName))
                this.gameItem = GameItemManager.Instance[containerNode.GameItemName];
            else
                this.gameItem = gameItem;

            containerNodeMonitorDictionary = new Dictionary<ContainerNode, ContainerNodeMonitor>();
            monitoredLinks = new List<LinkMonitor>();

            this.containerNode.NodeAdded += (NodeBase node) =>
            {
                if (node is ContainerNode)
                    AddContainerNodeMonitor((ContainerNode)node);
            };
            this.containerNode.NodeRemoved += (NodeBase node) =>
            {
                if (node == CurrentNode)
                {
                    UnloadCurrentNode();
                    StartFromEntryNode();
                }

                if (node is ContainerNode)
                    RemoveContainerNodeMonitor((ContainerNode)node);
            };
            this.containerNode.LinkAdded += (container, link) =>
            {
                if (RunningStatus != RunningStatus.Running || CurrentNode != link.StartingNode)
                    return;

                LinkMonitor linkMonitor = RegisterLink(link);

                if (linkMonitor.IsLinkEstablished)
                    LinkMonitor_LinkEstablished(linkMonitor);
            };

            LoadContainerNode();
        }

        public void Start()
        {
            if (RunningStatus == RunningStatus.Running)
                return;

            RunningStatus = RunningStatus.Running;
            StartFromEntryNode();
        }

        public void Stop()
        {
            if (RunningStatus == RunningStatus.Stopped)
                return;

            RunningStatus = RunningStatus.Stopped;
            UnloadCurrentNode();
        }

        public void Pause()
        {
            RunningStatus = RunningStatus.Paused;

            if (CurrentNode != null)
            {
                if (CurrentNode is ContainerNode)
                    CurrentContainerNodeMonitor.Pause();

                UnregisterMonitoredLinks();
            }
        }

        public void Resume()
        {
            if (CurrentNode == null)
                Start();
            else
            {
                RunningStatus = RunningStatus.Running;

                if (CurrentNode is ContainerNode)
                    CurrentContainerNodeMonitor.Resume();

                RegisterLinksToMonitor(CurrentNode.BoundLinks);

                LinkMonitor linkMonitor = monitoredLinks.FirstOrDefault(x => x.IsLinkEstablished);

                if (linkMonitor != null)
                    LinkMonitor_LinkEstablished(linkMonitor);
            }
        }

        public T ExtractNodeByType<T>() where T : NodeBase
        {
            if (CurrentNode is T)
                return (T)CurrentNode;
            else
            {
                try { return CurrentContainerNodeMonitor.ExtractNodeByType<T>(); }
                catch { return null; }
            }
        }

        public void UpdateLockMontior(ILock lockItem)
        {
            List<LockMonitorBase> lockMonitorsToUpdate = new List<LockMonitorBase>();

            foreach (LinkMonitor linkMonitor in monitoredLinks)
                lockMonitorsToUpdate.AddRange(linkMonitor.LockMonitorList.Values.Where((lockMonitor) => lockMonitor.Lock == lockItem));

            foreach (LockMonitorBase lockMonitor in lockMonitorsToUpdate)
                lockMonitor.UpdateLock();

            if (CurrentNode is ContainerNode)
                CurrentContainerNodeMonitor.UpdateLockMontior(lockItem);
        }

        public void UpdateGameDataOfNode(NodeBase node)
        {
            if (CurrentNode == node)
                ExecuteNode(CurrentNode);
            else if (CurrentNode is ContainerNode)
                CurrentContainerNodeMonitor.UpdateGameDataOfNode(node);
        }

        public bool IsCurrentNode(NodeBase node)
        {
            if (CurrentNode == node)
                return true;
            else if (CurrentNode is ContainerNode)
                return CurrentContainerNodeMonitor.IsCurrentNode(node);
            else
                return false;
        }

        private void StartFromEntryNode()
        {
            LogNode(containerNode.EntryNode, "Entering Container");
            UpdateCurrentNode(containerNode.EntryNode);
        }

        private void LoadContainerNode()
        {
            foreach (NodeBase node in containerNode.NodeCollection)
            {
                if (node is ContainerNode)
                    AddContainerNodeMonitor((ContainerNode)node);
            }
        }

        private void AddContainerNodeMonitor(ContainerNode containerNode)
        {
            ContainerNodeMonitor level = new ContainerNodeMonitor(ParentNodeScript, containerNode, gameItem, tracker);
            level.BubbleUpNodeLoaded += new Action<ContainerNodeMonitor, NodeBase>(ContainerLevel_BubbleUpNodeLoaded);
            level.BubbleUpNodeUnloaded += new Action<ContainerNodeMonitor, NodeBase>(ContainerLevel_BubbleUpNodeUnloaded);
            level.BubbleUpNodeExecuted += new Action<ContainerNodeMonitor, NodeBase>(ContainerLevel_BubbleUpNodeExecuted);
            containerNodeMonitorDictionary.Add(containerNode, level);

            if (RunningStatus == RunningStatus.Running)
                level.Start();
        }

        private void RemoveContainerNodeMonitor(ContainerNode containerNode)
        {
            ContainerNodeMonitor level = containerNodeMonitorDictionary[containerNode];
            level.BubbleUpNodeLoaded -= new Action<ContainerNodeMonitor, NodeBase>(ContainerLevel_BubbleUpNodeLoaded);
            level.BubbleUpNodeUnloaded -= new Action<ContainerNodeMonitor, NodeBase>(ContainerLevel_BubbleUpNodeUnloaded);
            level.BubbleUpNodeExecuted -= new Action<ContainerNodeMonitor, NodeBase>(ContainerLevel_BubbleUpNodeExecuted);
            containerNodeMonitorDictionary.Remove(containerNode);
        }

        private void UpdateCurrentNode(NodeBase currentNode)
        {
            if (isUpdating)
            {
                LogInfiniteLoopException(currentNode, " Attempting to Update CurrentNode that is already in the processs of being updated.");
                return;
            }

            isUpdating = true;

            UnloadCurrentNode();
            CurrentNode = DetermineNodeToMonitor(containerNode[currentNode.Name]);
            LoadCurrentNode();

            isUpdating = false;
        }

        private void LoadCurrentNode()
        {
            if (CurrentNode == null)
                return;

            LogNode(CurrentNode, "Node Loaded");

            RegisterLinksToMonitor(CurrentNode.BoundLinks);

            if (CurrentNode is ContainerNode)
                CurrentContainerNodeMonitor.Start();

            OnBubbleUpNodeLoaded(CurrentNode); 
        }

        private void UnloadCurrentNode()
        {
            if (CurrentNode == null)
                return;

            LogNode(CurrentNode, "Node Unloaded");

            UnregisterMonitoredLinks();

            if (CurrentNode is ContainerNode)
                CurrentContainerNodeMonitor.Stop();

            NodeBase tempForNotification = CurrentNode;
            CurrentNode = null;

            OnBubbleUpNodeUnloaded(tempForNotification);
        }

        private void RegisterLinksToMonitor(IEnumerable<Link> linkCollection)
        {
            foreach (Link link in linkCollection)
                RegisterLink(link);
        }

        private LinkMonitor RegisterLink(Link link)
        {
            LinkMonitor linkMonitor = new LinkMonitor(link, gameItem);
            linkMonitor.LinkEstablished += new Action<LinkMonitor>(LinkMonitor_LinkEstablished);
            linkMonitor.Disposed += new EventHandler(LinkMonitor_Disposed);
            monitoredLinks.Add(linkMonitor);

            return linkMonitor;
        }

        private void UnregisterMonitoredLinks()
        {
            while (0 < monitoredLinks.Count)
                monitoredLinks[0].Dispose();
        }

        private NodeBase DetermineNodeToMonitor(NodeBase node)
        {
            NodeBase returnNode = node;
            int loopAmount = 0;

            while (true)
            {
                ExecuteNode(node);

                if (RunningStatus != RunningStatus.Running)
                    return null;

                node = ObtainLinkedNode(node);

                if (node != null)
                    returnNode = node;
                else
                    break;

                if (200 < loopAmount++)
                {
                    LogInfiniteLoopException(CurrentNode, "");
                    return node;
                }
            }

            return returnNode;
        }

        private void ExecuteNode(NodeBase node)
        {
            LogNode(node, "Execute Node");
            LogGameData(node);

            isExecuting = true;
            node.UpdateGameToolData(gameItem);
            isExecuting = false;

            OnBubbleUpNodeExecuted(node);
        }

        private NodeBase ObtainLinkedNode(NodeBase nodeBase)
        {
            foreach (Link link in nodeBase.BoundLinks)
            {
                LinkMonitor linkMonitor = new LinkMonitor(link, gameItem);

                if (linkMonitor.IsLinkEstablished)
                {
                    LogEstablishedLink(link.EndingNode, linkMonitor);
                    linkMonitor.Dispose();
                    return link.EndingNode;
                }

                linkMonitor.Dispose();
            }

            return null;
        }

        private bool GetIsExecuting()
        {
            if (isExecuting)
                return true;

            if (CurrentNode is ContainerNode)
            {
                if (CurrentContainerNodeMonitor.IsExecuting)
                    return true;
            }

            return false;
        }

        private void OnBubbleUpNodeUnloaded(NodeBase currentNode)
        {
            if (BubbleUpNodeUnloaded != null)
                BubbleUpNodeUnloaded(this, currentNode);
        }

        private void OnBubbleUpNodeLoaded(NodeBase currentNode)
        {
            if (BubbleUpNodeLoaded != null)
                BubbleUpNodeLoaded(this, currentNode);
        }

        private void OnBubbleUpNodeExecuted(NodeBase node)
        {
            if (BubbleUpNodeExecuted != null)
                BubbleUpNodeExecuted(this, node);
        }

        private void LinkMonitor_LinkEstablished(LinkMonitor linkMonitor)
        {
            LogEstablishedLink(linkMonitor.MonitoredLink.EndingNode, linkMonitor);

            if (!IsExecuting)
                UpdateCurrentNode(linkMonitor.MonitoredLink.EndingNode);
            else
                PendingNode = linkMonitor.MonitoredLink.EndingNode;
        }

        private void LinkMonitor_Disposed(object sender, EventArgs e)
        {
            LinkMonitor linkMonitor = sender as LinkMonitor;

            linkMonitor.LinkEstablished -= new Action<LinkMonitor>(LinkMonitor_LinkEstablished);
            linkMonitor.Disposed -= new EventHandler(LinkMonitor_Disposed);
            monitoredLinks.Remove(linkMonitor);
        }

        private void ContainerLevel_BubbleUpNodeLoaded(ContainerNodeMonitor nodeMonitor, NodeBase node)
        {
            OnBubbleUpNodeLoaded(node);
        }

        private void ContainerLevel_BubbleUpNodeUnloaded(ContainerNodeMonitor nodeMonitor, NodeBase node)
        {
            OnBubbleUpNodeUnloaded(node);
        }

        private void ContainerLevel_BubbleUpNodeExecuted(ContainerNodeMonitor nodeMonitor, NodeBase node)
        {
            if (PendingNode != null)
            {
                NodeBase tempNode = PendingNode;
                PendingNode = null;
                UpdateCurrentNode(tempNode);
            }

            OnBubbleUpNodeExecuted(node);
        }

        private string GetTabAmount()
        {
            string tab = "";
            ContainerNode parentContainer = containerNode.ParentContainer;

            while (parentContainer != null)
            {
                tab += "   ";
                parentContainer = parentContainer.ParentContainer;
            }

            return tab;
        }

        public void LogEstablishedLink(NodeBase node, LinkMonitor establishedLink)
        {
            if (!tracker.IsEnabled)
                return;

            string trackLog = GetTabAmount() + "[Container: " + node.ParentContainer.Name + ", Node: " + node.Name + "]" + " OpenCriteria: " + establishedLink.MonitoredLink.OpenCriteria;

            foreach (LockMonitorBase lockItem in establishedLink.LockMonitorList.Values)
            {
                trackLog += ", Lock: " + lockItem.ToString();

                if (lockItem.IsOpen)
                    trackLog += "(Open)";
                else
                    trackLog += "(Closed)";
            }

            tracker.WriteLine(trackLog);
        }

        public void LogGameData(NodeBase node)
        {
            if (!tracker.IsEnabled || node.GameDataCollection.Count == 0)
                return;

            tracker.WriteLine(GetTabAmount() + "[Container: " + node.ParentContainer.Name + ", Node: " + node.Name + "]" + " Changing the following GameTool Data:");

            foreach (GameData data in node.GameDataCollection)
            {
                string trackLog = GetTabAmount() + "   - [GameTool: " + data.GameToolName + "] ";

                foreach (string propertyName in data.PropertyDictionary.Keys)
                    trackLog += propertyName + "=" + GameEngine.GetStringRepresention(data.PropertyDictionary[propertyName]) + ", ";

                tracker.WriteLine(trackLog);
            }
        }

        public void LogNode(NodeBase node, string data)
        {
            string parentContainerName = "Null";

            if (node.ParentContainer != null)
                parentContainerName = node.ParentContainer.Name;

            tracker.WriteLine(GetTabAmount() + "[Container: " + parentContainerName + ", " + node.GetType().Name + ": " + node.Name + "] " + data);
        }

        private void LogInfiniteLoopException(NodeBase CurrentNode, string additionalMessage)
        {
            var parentName = "???";
            var nodeName = "???";

            if (CurrentNode != null)
            {
                nodeName = CurrentNode.Name;

                if (CurrentNode.ParentContainer != null)
                    parentName = CurrentNode.ParentContainer.Name;
            }

            tracker.WriteLine(GetTabAmount() + "!!!!!!! WARNING: Infinite loop !!!!!!! [Container: " + parentName + ", Node: " + nodeName + "]");
        }
    }
}
